/**
 * Copyright (C) 2001-2020 by RapidMiner and the contributors
 * 
 * Complete list of developers available at our web site:
 * 
 * http://rapidminer.com
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.operator.meta;

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.operator.ProcessSetupError.Severity;
import com.rapidminer.operator.SimpleProcessSetupError;
import com.rapidminer.operator.UserError;
import com.rapidminer.operator.ports.DummyPortPairExtender;
import com.rapidminer.operator.ports.PortPairExtender;
import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeList;
import com.rapidminer.parameter.ParameterTypeString;
import com.rapidminer.parameter.UndefinedParameterError;


/**
 * Sets a list of parameters using existing parameter values. <br/>
 *
 * The operator is similar to {@link ParameterSetter}, but differs from that in not requiring a
 * ParameterSet input. It simply reads a parameter value from a source and uses it to set the
 * parameter value of a target parameter. Both, source and target, are given in the format
 * 'operator'.'parameter'. <br/>
 *
 * This operator is more general than ParameterSetter and could completely replace it. It is most
 * useful, if you need a parameter which is optimized more than once within the optimization loop -
 * ParameterSetter cannot be used here. <br/>
 *
 * These parameters can either be generated by a {@link ParameterOptimizationOperator} or read by a
 * {@link com.rapidminer.extension.legacy.operator.io.ParameterSetLoader}. This operator is useful,
 * e.g. in the following scenario. If one wants to find the best parameters for a certain learning
 * scheme, one usually is also interested in the model generated with this parameters. While the
 * first is easily possible using a {@link ParameterOptimizationOperator}, the latter is not
 * possible because the {@link ParameterOptimizationOperator} does not return the IOObjects produced
 * within, but only a parameter set. This is, because the parameter optimization operator knows
 * nothing about models, but only about the performance vectors produced within. Producing
 * performance vectors does not necessarily require a model. <br/>
 * To solve this problem, one can use a <code>ParameterSetter</code>. Usually, a process definition
 * with a <code>ParameterSetter</code> contains at least two operators of the same type, typically a
 * learner. One learner may be an inner operator of the {@link ParameterOptimizationOperator} and
 * may be named &quot;Learner&quot;, whereas a second learner of the same type named
 * &quot;OptimalLearner&quot; follows the parameter optimization and should use the optimal
 * parameter set found by the optimization. In order to make the <code>ParameterSetter</code> set
 * the optimal parameters of the right operator, one must specify its name. Therefore, the parameter
 * list <var>name_map</var> was introduced. Each parameter in this list maps the name of an operator
 * that was used during optimization (in our case this is &quot;Learner&quot;) to an operator that
 * should now use these parameters (in our case this is &quot;OptimalLearner&quot;).
 *
 * @author Robert Rudolph
 */
public class ParameterCloner extends Operator {

	/**
	 * The parameter name for &quot;A list mapping operator parameters from the set to other
	 * operator parameters in the process setup.&quot;
	 */
	public static final String PARAMETER_NAME_MAP = "name_map";

	private PortPairExtender dummyPorts = new DummyPortPairExtender("through", getInputPorts(), getOutputPorts());

	public ParameterCloner(OperatorDescription description) {
		super(description);

		dummyPorts.start();

		getTransformer().addRule(dummyPorts.makePassThroughRule());
	}

	@Override
	public void doWork() throws OperatorException {
		List<String[]> nameList = getParameterList(PARAMETER_NAME_MAP);
		Iterator<String[]> i = nameList.iterator();
		while (i.hasNext()) {
			String[] keyValue = i.next();
			String[] source = keyValue[0].split("\\.");
			String[] target = keyValue[1].split("\\.");
			if (source.length != 2) {
				throw new UserError(this, 907, keyValue[0]);
			}
			if (target.length != 2) {
				throw new UserError(this, 907, keyValue[1]);
			}
			Operator operator = lookupOperator(source[0]);
			if (operator == null) {
				throw new UserError(this, 109, source[0]);
			}
			String value = operator.getParameter(source[1]);
			if (value == null) {
				throw new UserError(this, 213, new Object[] { source[1], source[0], this.getName() });
			}
			operator = lookupOperator(target[0]);
			if (operator == null) {
				throw new UserError(this, 109, target[0]);
			}
			operator.getParameters().setParameter(target[1], value);
		}

		dummyPorts.passDataThrough();
	}

	@Override
	public int checkProperties() {
		int errorNumber = 0;
		try {
			final List<String[]> nameList = getParameterList(PARAMETER_NAME_MAP);
			;
			if (nameList.size() > 0) {
				// check if valid operator names are present
				HashSet<String> operatorNames = new HashSet<String>();
				operatorNames.addAll(getProcess().getAllOperatorNames());
				String nameMap = getParameterAsString(PARAMETER_NAME_MAP);
				for (final String[] pair : nameList) {
					for (String operatorParameter : pair) {
						String[] parts = operatorParameter.split("\\.");
						if (parts.length != 2) {
							errorNumber++;
							addError(new SimpleProcessSetupError(Severity.ERROR, this.getPortOwner(),
									Collections
											.singletonList(new ParameterSettingQuickFix(this, PARAMETER_NAME_MAP, nameMap)),
									"parameter_wrong_format", PARAMETER_NAME_MAP));
							break;
						} else {
							if (!operatorNames.contains(parts[0])) {
								errorNumber++;
								addError(new SimpleProcessSetupError(Severity.ERROR, this.getPortOwner(),
										Collections.singletonList(
												new ParameterSettingQuickFix(this, PARAMETER_NAME_MAP, nameMap)),
										"parameter_unknown_operator", PARAMETER_NAME_MAP, parts[0]));
							} else {
								Operator operator = lookupOperator(parts[0]);
								ParameterType type = operator.getParameterType(parts[1]);
								if (type == null) {
									errorNumber++;
									addError(new SimpleProcessSetupError(Severity.ERROR, getPortOwner(),
											Collections.singletonList(
													new ParameterSettingQuickFix(this, PARAMETER_NAME_MAP, nameMap)),
											"parameter_unknown_parameter_for_operator", parts[1], parts[0]));
								}
							}
						}
					}
				}
			} else {
				// list empty: add quickfix for opening definition window
				errorNumber++;
				addError(new SimpleProcessSetupError(Severity.WARNING, this.getPortOwner(),
						Collections.singletonList(new ParameterSettingQuickFix(this, PARAMETER_NAME_MAP, "")),
						"parameter_list_undefined", "the mapping of operator names"));
			}
		} catch (UndefinedParameterError e) {
			// list is undefined: Add quickfix for opening definition window
			errorNumber++;
			addError(new SimpleProcessSetupError(Severity.WARNING, this.getPortOwner(),
					Collections.singletonList(new ParameterSettingQuickFix(this, PARAMETER_NAME_MAP, "")),
					"parameter_list_undefined", "the mapping of operator names"));
		}
		return errorNumber + super.checkProperties();
	}

	@Override
	public List<ParameterType> getParameterTypes() {
		List<ParameterType> types = super.getParameterTypes();
		ParameterType type = new ParameterTypeList(PARAMETER_NAME_MAP,
				"A list mapping operator parameters from the set to other operator parameters in the process setup.",
				new ParameterTypeString("source",
						"The source parameter, specified as 'operator'.'parameter'. This value is copied to the target parameter."),
				new ParameterTypeString("target", "The target parameter, specified as 'operator'.'parameter'."), false);
		type.setPrimary(true);
		types.add(type);
		return types;
	}
}
